<?php 
/**
 * This file is part of DomAr.
 * 
 * DomAr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * DomAr is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with DomAr.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @link http://inditel.ee/
 * @copyright 2007-2009 Inditel Meedia OÜ
 * @author Oliver Leisalu <oliver@inditel.ee>
 */

/**
* Plugin that helps to control date-s.
* @package DomAr
* @subpackage Plugin
*/
class DomArAclRole extends DomArPlugin {
	
	/**
	* @param Array [name => Boolean] true if allowed, false if denied.
	*/
	private $resources = array();

	/**
	* @param Array [name => true]
	*/
	private $changedResources = array();
	
	/**
	* @param Boolean if resources are loaded from database.
	*/
	private $resourcesLoaded = false;
	
	/**
	* @param Array [id => DomArAclRole]
	*/
	private $parents = array();
	
	/**
	* @param Array [id => DomArObject]
	*/
	private $newParents = array();
	
	/**
	* @param Boolean if parents are loaded from database.
	*/
	private $parentsLoaded = false;
	
	/**
	* @param Int
	* Max recursive levels that will be checked for parent relationhips.
	*/
	const MAX_LEVELS = 8;
	
	/**
	* @param DomArObject $parent
	* @param DomArProperty $property
	*/
	public function __construct( DomArObject $parent, DomArProperty $property ) {
		parent::__construct( $parent, $property );
	}
	
	
	// --------------------------------------------------------
	// RESOURCES
	// --------------------------------------------------------
	
	/**
	* Allow some resource. If allowed then nothing can disallow it.
	* @param String $resource
	* @param String $value
	*/
	public function allow( $resource, $value = NULL ) {
		if( $value === NULL)
			$this->resources[$resource] = true;
		else
			$this->resources[$resource] = array('allow'=>true, 'value'=>$value);
		$this->changedResources[$resource] = true;
		$this->changed();
	}
	
	/**
	* Deny some resource. If denied then nothing can allow it.
	* @param String $resource
	* @param String $value
	*/
	public function deny( $resource, $value = NULL ) {

		if( $value === NULL)
			$this->resources[$resource] = false;
		else
			$this->resources[$resource] = array('allow'=>false, 'value'=>$value);
		
		$this->changedResources[$resource] = true;
		$this->changed();
	} 
	
	/**
	* Set some resource to allowed(true) or not allowed(false).
	* @param String $resource
	* @param Bool $status
	* @param String $value
	*/
	public function set( $resource, $status, $value = NULL ) {
		if( $status )
			$this->allow( $resource, $value );
		else
			$this->deny( $resource, $value );
	}
	
	/**
	* Remove resource. if asked for allowed/denied then parents will be checked for allowance.
	* @param String
	*/
	public function remove( $resource ) {
		unset($this->resources[$resource]);
		unset($this->changedResources[$resource]);
		$this->getProperty()->removeResource( $this, $resource );
		$this->changed();
	}
	
	/**
	* If resource is allowed
	* @param String
	* @return Boolean
	*/
	public function isAllowed( $resource ) {
		return $this->searchResource( $resource, false);
	}
	
	/**
	* Return resource value. 
	* @param String $resource
	* @return Mixed
	* Returns
	* true - 	Is allowed, value is not set
	* false -	Is not allowed, value is not set
	* value -	Value. Notice that value is returned even if not allowed! 
	* 			For negative test you should use isAllowed() method.
	*/
	public function getValue( $resource ) {
		return $this->searchResource( $resource, true);
	}
	
	/**
	* Helper for getValue and isAllowed method.
	* @param String $resource
	* @param String $value
	* @param Boolean $returnValue
	*/
	private function searchResource( $resource, $returnValue ) {
		
		$this->loadResources();
		if( isset($this->resources[$resource]) === true ) {
			if( is_array($this->resources[$resource]) === true )
				return $this->resources[$resource][($returnValue === true ? 'value' : 'allow')];
			else
				return $this->resources[$resource];
		}
		
		// disable loops
		static $level;
		if( ++$level > self::MAX_LEVELS )
			return false;
	
		// check parents
		$this->loadParents();	
		foreach( $this->parents as $parent ) {
			if( $parent->isAllowed($resource) === true ) {
				$level--;
				return true;
			}
		}
		
		$level--;
		return false;
	}
	
	/**
	* If resource is unknown. Parents will not be checked, it is for determining if item has resource overruled!
	* @param String
	*/
	public function isUnknown( $resource ) {
		$this->loadResources();
		return !isset($this->resources[$resource]);
	}
	
	/**
	* Loads resources from database. DomArAcl::$resourcesLoaded must be false to actually make query.
	*/
	protected function loadResources() {
		if( $this->resourcesLoaded === false ) {
			foreach( $this->getProperty()->loadResources( $this ) as $key => $value ) {
				if( $this->changedResources[$key] !== true )
					$this->resources[$key] = $value;
			}
			$this->resourcesLoaded = true;
		}
	}
	
	// --------------------------------------------------------
	// ROLES
	// --------------------------------------------------------
	
	/**
	* Adds parent. Parent must belong to same ACL group.
	* @param DomArObject $object
	*/
	public function addParent( DomArObject $object ) {
		if( $this->validateParent($object) === true ) {
			$this->changed();
			$this->newParents[$object->getId()] = $object;
			$this->parents[$object->getId()] = $object->{$this->getParentAclProperty($object)->name};
		} else {
			throw new RuntimeException('Parent does not valideta! Does not belong to same group or is not saved (Doesn\'t have id).');
		}
	}
	
	/**
	* Removes parent
	* @param DomArObject $object
	*/
	public function removeParent( DomArObject $object ) {
		$aclProperty = $this->getParentAclProperty($object);
		if( is_object($aclProperty) === true ) {
			$this->unloadParent( $object );
			$this->getProperty()->removeParent( $this, $object->{$this->getParentAclProperty($object)->name} );
		}
	}
	
	/**
	* Unloads parent. Sets DomArAcl::$parentsLoaded to false and removes parent from loaded parents. Doesn't remove parent from database!
	* This is meant for internal use and usually should not be needed!
	* @param DomArObject $object
	*/
	public function unloadParent( DomArObject $object ) {
		unset($this->parents[$object->getId()]);
		unset($this->newParents[$object->getId()]);
		$this->parentsLoaded = false;
	}
	
	/**
	* Checks if parent has ACL property from same group 
	* @param DomArObject $object
	* @return Boolean
	*/
	public function validateParent( DomArObject $object ) {
		return (bool)($this->getParentAclProperty( $object ) && $object->getId() > 0);
	}
	
	/**
	* Returns parent ACl property
	* @param DomArObject $object
	* @return DomArProperty_DomArAcl
	*/
	public function getParentAclProperty( DomArObject $object ) {
		$property = $object->getProperty( $this->getProperty()->name );
		if( $property instanceof DomArProperty_DomArAcl && $property->groupName == $this->getProperty()->groupName )
			return $property;
		else {
			foreach( $object->getProperties() as $property ) {
				if( $property instanceof DomArProperty_DomArAcl && $property->groupName == $this->getProperty()->groupName )
					return $property;
			}
		}
		return NULL;
	}
	
	/**
	* Loads parents from database. DomArAcl::$parentsLoaded must be false to actually make query.
	*/
	protected function loadParents() {
		if( $this->parentsLoaded === false ) {
			foreach( $this->getProperty()->loadParents( $this ) as $id => $parent ) {
				$this->parents[$id] = $parent;
			}
			$this->parentsLoaded = true;
		}
	}
	
	
	
	// --------------------------------------------------------
	// RESOURCES
	// --------------------------------------------------------
	
	public function __get( $var ) {
		return $this->$var;
	}
	
	/**
	* This method DO NOT take back changes made to ACL!!!
	*/
	public function clearChangedResourcesList() {
		$this->changedResources = array();
	}
	
	/**
	* This method is meant for internal or advanced use only!
	* This method may change in future without notice!
	*/
	public function &access( $var ) {
		return $this->$var;
	}
	
	/**
	* Shows debug info about rights. Uses debug method to show it.
	* For debugging only! No input is needed!
	*/
	public function debugTree( array &$tree = array(), $i = 1, $parent = NULL ) {
	
		$tree[] = array('level'=>$i, 'name'=>get_class($this->getParent()).'('.$this->getParent()->id.')', 'resources'=>$this->resources, 'parent'=>get_class($parent).'('.$parent->id.')' );


		// disable loops
		static $level;
		if( ++$level > self::MAX_LEVELS ) {
			$tree[] = array('level'=>$i+1, 'name'=>'max level reached, stopped checking deeper! (might be recursion)', 'parent'=>$this->getParent()->name);
			return; 
		}
		
		foreach( $this->parents as $parent ) {
			//$tree[] = array('level'=>$i, 'name'=>$this->getParent()->name, 'resources'=>$this->resources, 'parent'=>$parent->name );
			$parent->debugTree( $tree, $i+1, $this->getParent() );
		}
		
		$level--;
		
		if( $i == 1 ) 
			debug($tree);
		return $tree;
	}
	
	
}

/**
* @link http://inditel.ee/
* @copyright 2007-2009 Inditel Meedia OÜ
* @author Oliver Leisalu <oliver@inditel.ee>
* @package DomAr
* @subpackage Plugin
*/
class DomArProperty_DomArAcl extends DomArProperty_Plugin implements IDomArPropertyAlterTableListener,IDomArPropertyInsertListener,IDomArPropertyUpdateListener, IDomArPropertySerializeListener, IDomArPropertyBeforeSetListener, IDomArPropertyBeforeDeleteListener {
	
	/**
	* @param String 
	* Basicly tablename suffix. For items that will be related this must be same!
	*/
	protected $groupName;
	
	public function __construct( $name, $groupName = 'default' ) {
		parent::__construct( $name );
		$this->groupName = $groupName;
	}
	
	public function init( DomArObject $object, array &$values ) {
		return NULL;
	}
	
	public function load( DomArObject $object, &$propertyValue ) {
		return new DomArAclRole( $object, $this );
	}
	
	public function beforeSet( DomArObject $object, &$oldPropertyValue, &$newPropertyValue ) {
		throw new RuntimeException('Setting Acl list is not allowed!');
	}
	
	public function isLoaded( DomArObject $object, &$propertyValue ) {
		return is_object($propertyValue);
	}
	
	public function validate( DomArObject $object, &$propertyValue, array &$customParams = array() ) {
		return spl_object_hash($propertyValue) == spl_object_hash($object->{$this->name}) ? 0 : 1;
	}
	
	public function insert( DomArObject $object, &$acl, DomArValueSet $set ) {
		//$this->afterUpdate( $object, $acl );
		$object->setChanged( $this->name, true);
	}
	
	public function update( DomArObject $object, &$acl, DomArValueSet $set  ) {
		//debug($acl);
		//if( !is_object($acl) ) echo exceptionToString( new Exception() );
		if( $acl === NULL )
			return;
		
			
		// save resources
		foreach( $acl->changedResources as $resource => $value ) {
			if( is_array($acl->resources[$resource] ) ) {
				$status = (int)$acl->resources[$resource]['allow'];
				$value = '\''.addslashes($acl->resources[$resource]['value']).'\'';
			} else {
				$status = (int)$acl->resources[$resource];
				$value = 'NULL';
			}
			
			try {
				DomSql::query('INSERT INTO '.$this->getResourcesTableName().' (parentId, parentClassName, resource, status, value) VALUES ('.(int)$object->getId().',\''.get_class($acl->getParent()).'\', \''.addslashes($resource).'\','.$status.', '.$value.')');
			} catch( RuntimeException $e ) {
				DomSql::query('UPDATE '.$this->getResourcesTableName().' SET status = '.$status.', value = '.$value.' WHERE parentId = '.$acl->getParent()->getId().' AND parentClassName = \''.get_class($acl->getParent()).'\' AND resource = \''.addslashes($resource).'\' LIMIT 1');
			}
		}
		//debug($acl);
		$acl->clearChangedResourcesList();
		
		// save new parents
		foreach( $acl->newParents as $parent ) {
			try {
				DomSql::query('INSERT INTO '.$this->getRelationsTableName().' (parentId, parentClassName, childId, childClassName) VALUES ('.(int)$parent->getId().',\''.get_class($parent).'\', '.(int)$acl->getParent()->getId().',\''.get_class($acl->getParent()).'\')');
			} catch( RuntimeException $e ) {
				throw $e;
			}
		}	
	}

	public function beforeDelete( DomArObject $object, &$acl ) {
		if( $object->getId() > 0) {
			DomSql::query('DELETE FROM '.$this->getResourcesTableName().' WHERE parentId = '.$object->getId().' AND parentClassName = \''.get_class($acl->getParent()).'\'');
			foreach( $this->loadChildren($acl) as $child ) {
				$child->unloadParent( $object );
			}
			DomSql::query('DELETE FROM '.$this->getRelationsTableName().' WHERE (childId = '.$object->getId().' AND childClassName = \''.get_class($acl->getParent()).'\') or (parentId = '.$object->getId().' AND parentClassName = \''.get_class($acl->getParent()).'\')');
		}
	}

	public function alterTable( DomArTable $table ) {
		
		// make resources table
		$aclTable = new DomSqlTable( $this->getResourcesTableName() );
		$aclTable->update();
		$aclTable->addColumn('parentId','bigint',false);
		$aclTable->addColumn('parentClassName','varchar(50)',false);
		$aclTable->addColumn('resource','varchar(60)',false);
		$aclTable->addColumn('status','tinyint(1)',false);
		$aclTable->addColumn('value','varchar(50)',true);
		$aclTable->dropColumn('id');
		$aclTable->addIndex( 'aclIndex', array('parentId','parentClassName','resource'), 'unique' );
		
		
		// make relations table
		$aclTable = new DomSqlTable( $this->getRelationsTableName() );
		$aclTable->update();
		$aclTable->addColumn('parentId','bigint',false);
		$aclTable->addColumn('parentClassName','varchar(50)',false);
		$aclTable->addColumn('childId','bigint',false);
		$aclTable->addColumn('childClassName','varchar(50)',false);
		$aclTable->dropColumn('id');
		$aclTable->addIndex( 'aclIndex', array('parentId','parentClassName','childId','childClassName'), 'unique' );
	}
	
	

	public function serialize( DomArObject $object, $acl ) {
		if( is_object($acl) ) {
			
			$parents = &$acl->access('parents');
			$n = array();
			foreach( $parents as $id => $object )
				$n[$id] = get_class($object->getParent());
			$parents = $n;
			
			
			$newParents = &$acl->access('newParents');
			$n = array();
			foreach( $newParents as $id => $object )
				$n[$id] = get_class($object);
			$newParents = $n;
			
		}
		return $acl;
	}
	
	public function unserialize( DomArObject $object, $acl ) {
		
		$parents = &$acl->access('parents');
		$n = array();
		foreach( $parents as $id => $className ) {
			$parent = call_user_func( array($className, 'load'), $id );
			if( $parent )
				$n[$id] = $parent->{$acl->getParentAclProperty( $parent )->name};
		}
		$parents = $n;
		
		
		$newParents = &$acl->access('newParents');
		$n = array();
		foreach( $newParents as $id => $className ) {
			$parent = call_user_func( array($className, 'load'), $id );
			if( $parent )
				$n[$id] = $parent;
		}
		$newParents = $n;
			
		
		
		
		return $acl;
	}
	
	
	
	
	
	/**
	* @return String
	*/
	public function getResourcesTableName() {
		return 'domar_acl_resources_'.$this->groupName;
	}
	
	/**
	* @return String
	*/
	public function getRelationsTableName() {
		return 'domar_acl_relations_'.$this->groupName;
	}
	
	/**
	* Removes resource from Role.
	* @param DomArAclRole $acl
	* @param String $resource
	*/
	public function removeResource( DomArAclRole $acl, $resource ) {
		if( $acl->getParent()->getId() > 0)
			DomSql::query('DELETE FROM '.$this->getResourcesTableName().' WHERE parentId = '.$acl->getParent()->getId().' AND parentClassName = \''.get_class($acl->getParent()).'\' AND resource = \''.$resource.'\' LIMIT 1');
	}
	
	/**
	* @param DomArAclRole $acl
	* @return Array
	*/
	public function loadResources( DomArAclRole $acl ) {
		if( $acl->getParent()->getId() > 0 ) {
			$resources = array();
			foreach( DomSql::selectQuery('SELECT resource, status, value FROM '.$this->getResourcesTableName().' WHERE parentId = '.$acl->getParent()->getId().' AND parentClassName = \''.get_class($acl->getParent()).'\' ') as $dat ) {
				if( $dat['value'] == NULL ) {
					$resources[$dat['resource']] = (bool)$dat['status'];
				} else {
					$resources[$dat['resource']] = array('allow'=>(bool)$dat['status'], 'value'=>$dat['value']);
				}
			}
			return $resources;
		}
		return array();
	}
	
	/**
	* @param DomArAclRole $acl
	* @return DomArAclRole[]
	*/
	public function loadParents( DomArAclRole $acl ) {
		if( $acl->getParent()->getId() > 0 ) {
			$parents = array();
			foreach( DomSql::selectQuery('SELECT parentId, parentClassName FROM '.$this->getRelationsTableName().' WHERE childId = '.$acl->getParent()->getId().' AND childClassName = \''.get_class($acl->getParent()).'\' ') as $dat ) {
				$parent = call_user_func( array($dat['parentClassName'],'load'), $dat['parentId'] );
				if( is_object($parent) === true ) {
					$parents[$parent->getId()] = $parent->{$acl->getParentAclProperty( $parent )->name};
				}
			}
			return $parents;
		}
		return array();
	}
	
	/**
	* @param DomArAclRole $acl
	* @return DomArAclRole[]
	*/
	public function loadChildren( DomArAclRole $acl ) {
		if( $acl->getParent()->getId() > 0 ) {
			$children = array();
			foreach( DomSql::selectQuery('SELECT childId, childClassName FROM '.$this->getRelationsTableName().' WHERE parentId = '.$acl->getParent()->getId().' AND parentClassName = \''.get_class($acl->getParent()).'\' ') as $dat ) {
				$child = call_user_func( array($dat['childClassName'],'load'), $dat['childId'] );
				if( is_object($child) === true ) {
					$children[$child->getId()] = $child->{$acl->getParentAclProperty( $child )->name};
				}
			}
			return $children;
		}
		return array();
	}
	
	/**
	* Removes parent role from child role.
	* @param DomArAclRole $acl
	* @param DomArAclRole $parentRole
	*/
	public function removeParent( DomArAclRole $acl, DomArAclRole $parentRole ) {
		if( $acl->getParent()->getId() > 0 )
			DomSql::query('DELETE FROM '.$this->getRelationsTableName().' WHERE parentId = '.$parentRole->getParent()->getId().' AND parentClassName = \''.get_class($parentRole->getParent()).'\' AND childId = '.$acl->getParent()->getId().' AND childClassName = \''.get_class($acl->getParent()).'\' LIMIT 1');
	}
	
}
?>